package com.zeus.gen.generate;

import com.zeus.gen.base.exception.ZeusGenException;
import com.zeus.gen.generate.model.DbColumn;
import com.zeus.gen.generate.model.DbTable;
import com.zeus.gen.generate.model.Generate;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.apache.ibatis.type.JdbcType;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * @author liuwei
 * @version 2018/1/19 下午2:03
 * @description 功能描述
 * @see
 * @since 1.0
 */
public class CodeGenerator {

    private static String dtoVm = "template/Dto.java.vm";
    private static String modelVm = "template/Model.java.vm";
    private static String mapperVm = "template/Mapper.xml.vm";
    private static String serviceVm = "template/Service.java.vm";
    private static String serviceImplVm = "template/ServiceImpl.java.vm";
    private static String controllerVm = "template/Controller.java.vm";
    private static String indexVm = "template/index.html.vm";
    private static String listVm = "template/list.html.vm";

    public static void generate(Generate generate, DbTable dbTable, ZipOutputStream zip) {
        String modelName = getModelName(generate.getTablePrefix(), dbTable);
        generate.setModelName(modelName);
        generate.setPackageName(generate.getBasePackageName() + "." + StringUtils.lowerCase(modelName));

        VelocityContext dataModel = new VelocityContext();
        dataModel.put("author", generate.getAuthor());
        dataModel.put("ModelName", modelName);
        dataModel.put("modelName", modelName.substring(0, 1).toLowerCase() + modelName.substring(1));
        dataModel.put("requestMapping", modelName.toLowerCase());
        dataModel.put("package", generate.getPackageName());
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        dataModel.put("time", sdf.format(new Date()));
        dataModel.put("comment", dbTable.getComment());

        Set<String> importSet = new HashSet<>();
        List<Map<String, Object>> fields = new ArrayList<>();
        for (DbColumn column : dbTable.getDbColumnList()) {
            Map<String, Object> columnInfo = new HashMap<>();
            columnInfo.put("name", StringUtils.uncapitalize(formatName(column.getName())));
            columnInfo.put("type", typeMapping(column.getDataType(), importSet));
            columnInfo.put("comment", column.getComment());
            //是否主键
            if ("PRI".equalsIgnoreCase(column.getColumnKey()) && dbTable.getPrimaryColumn() == null) {
                dbTable.setPrimaryColumn(column);
            }
            fields.add(columnInfo);
        }
        dataModel.put("fields", fields);
        dataModel.put("imports", importSet);

        //没主键，则第一个字段为主键
        if (dbTable.getPrimaryColumn() == null) {
            dbTable.setPrimaryColumn(dbTable.getDbColumnList().get(0));
        }

        //设置velocity资源加载器
        Properties prop = new Properties();
        prop.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
        Velocity.init(prop);

        if (generate.isChkDto()) {
            buildDto(generate, dataModel, zip);
        }
        if (generate.isChkModel()) {
            buildModel(generate, dataModel, zip);
        }
        if (generate.isChkMapper()) {
            buildMapper(generate, dataModel, dbTable, zip);
        }
        if (generate.isChkService()) {
            buildService(generate, dataModel, zip);
        }
        if (generate.isChkServiceImpl()) {
            buildServiceImpl(generate, dataModel, zip);
        }
        if (generate.isChkController()) {
            buildController(generate, dataModel, zip);
        }
        buildIndexPage(generate, modelName.toLowerCase(), dataModel, zip);
        buildListPage(generate, modelName.toLowerCase(), dataModel, zip);
    }

    /**
     * List模板
     *
     * @param generate
     * @param dataModel
     * @throws Exception
     */
    public static void buildListPage(Generate generate, String modualName, VelocityContext dataModel, ZipOutputStream zip) {
        //渲染模板
        StringWriter sw = new StringWriter();
        Template template = Velocity.getTemplate(listVm, "UTF-8");
        template.merge(dataModel, sw);
        try {
            //添加到zip
            zip.putNextEntry(new ZipEntry(getFileName(listVm, generate.getModelName(), generate.getPackageName(), modualName)));
            IOUtils.write(sw.toString(), zip, "UTF-8");
            IOUtils.closeQuietly(sw);
            zip.closeEntry();
        } catch (IOException e) {
            throw new ZeusGenException("渲染模板失败，表名：" + generate.getTableName(), e);
        }
    }

    /**
     * Index模板
     *
     * @param generate
     * @param dataModel
     * @throws Exception
     */
    public static void buildIndexPage(Generate generate, String modualName, VelocityContext dataModel, ZipOutputStream zip) {
        //渲染模板
        StringWriter sw = new StringWriter();
        Template template = Velocity.getTemplate(indexVm, "UTF-8");
        template.merge(dataModel, sw);
        try {
            //添加到zip
            zip.putNextEntry(new ZipEntry(getFileName(indexVm, generate.getModelName(), generate.getPackageName(), modualName)));
            IOUtils.write(sw.toString(), zip, "UTF-8");
            IOUtils.closeQuietly(sw);
            zip.closeEntry();
        } catch (IOException e) {
            throw new ZeusGenException("渲染模板失败，表名：" + generate.getTableName(), e);
        }
    }

    /**
     * Service模板
     *
     * @param generate
     * @param dataModel
     * @throws Exception
     */
    private static void buildService(Generate generate, VelocityContext dataModel, ZipOutputStream zip) {
        build(serviceVm, generate.getModelName(), generate.getPackageName(), null, dataModel, zip);
    }

    /**
     * Controller模板
     *
     * @param generate
     * @param dataModel
     * @throws Exception
     */
    public static void buildController(Generate generate, VelocityContext dataModel, ZipOutputStream zip) {
        build(controllerVm, generate.getModelName(), generate.getPackageName(), null, dataModel, zip);
    }

    /**
     * ServiceImpl模板
     *
     * @param generate
     * @param dataModel
     * @throws Exception
     */
    public static void buildServiceImpl(Generate generate, VelocityContext dataModel, ZipOutputStream zip) {
        build(serviceImplVm, generate.getModelName(), generate.getPackageName(), null, dataModel, zip);
    }


    /**
     * 查询dto
     *
     * @param generate
     * @param dataModel
     * @throws Exception
     */
    public static void buildDto(Generate generate, VelocityContext dataModel, ZipOutputStream zip) {
        build(dtoVm, generate.getModelName(), generate.getPackageName(), null, dataModel, zip);
    }

    /**
     * 实体类
     *
     * @param generate
     * @param dataModel
     * @throws Exception
     */
    public static void buildModel(Generate generate, VelocityContext dataModel, ZipOutputStream zip) {
        build(modelVm, generate.getModelName(), generate.getPackageName(), null, dataModel, zip);
    }

    /**
     * xml文件
     *
     * @param generate
     * @throws Exception
     */
    public static void buildMapper(Generate generate, VelocityContext dataModel, DbTable dbTable, ZipOutputStream zip) {
        String modelName = generate.getModelName();
        String packageName = generate.getPackageName();
        String tableName = generate.getTableName();
        // 设置参数
        dataModel.put("namespace", packageName + ".model." + modelName);
        dataModel.put("tableName", tableName);
        String keyProperty = StringUtils.uncapitalize(formatName(dbTable.getPrimaryColumn().getName()));
        dataModel.put("keyProperty", keyProperty);

        StringBuilder baseResultMap = new StringBuilder();
        StringBuilder baseWhereClause = new StringBuilder();
        StringBuilder baseUpdate = new StringBuilder();
        StringBuilder baseUpdateClause = new StringBuilder();
        StringBuilder columnList = new StringBuilder();
        StringBuilder insertColumnList = new StringBuilder();
        StringBuilder insertPropertyList = new StringBuilder();
        int size = dbTable.getDbColumnList().size();
        for (int i = 0; i < size; i++) {
            DbColumn column = dbTable.getDbColumnList().get(i);
            String propertyName = StringUtils.uncapitalize(formatName(column.getName()));
            JdbcType jdbcType = getJdbcType(column.getDataType());
            baseResultMap.append("<id column=\"" + column.getName() + "\" property=\"" + propertyName + "\" jdbcType=\"" + jdbcType.name() + "\"/>");
            baseWhereClause.append("<if test=\"params." + propertyName + " != null\">and " + column.getName() + " = #{params." + propertyName + ",jdbcType=" + jdbcType.name() + "}</if>");
            if (i < size - 1) {
                baseResultMap.append("\n        ");
                baseWhereClause.append("\n          ");
            }
            columnList.append(column.getName()).append(",");
            if (column.isAutoIncrement()) {
                continue;
            }
            buildInsert(insertColumnList, insertPropertyList, column, propertyName, jdbcType);
            if (column.isPrimaryKey()) {
                continue;
            }
            buildUpdate(baseUpdate, column, propertyName, jdbcType, i < size - 1);
            buildUpdateSelective(baseUpdateClause, column, propertyName, jdbcType);
        }
        dataModel.put("baseResultMap", baseResultMap);
        dataModel.put("baseWhereClause", baseWhereClause);
        dataModel.put("baseUpdate", baseUpdate);
        dataModel.put("baseUpdateClause", baseUpdateClause);
        dataModel.put("columnList", columnList.substring(0, columnList.length() - 1));
        dataModel.put("insertColumnList", insertColumnList.substring(0, insertColumnList.length() - 1));
        dataModel.put("insertPropertyList", insertPropertyList.substring(0, insertPropertyList.length() - 1));

        build(mapperVm, generate.getModelName(), generate.getPackageName(), null, dataModel, zip);
    }

    private static void buildInsert(StringBuilder insertColumnList, StringBuilder insertPropertyList,
                                    DbColumn column, String propertyName, JdbcType jdbcType) {
        insertColumnList.append(column.getName()).append(",");
        insertPropertyList.append("#{" + propertyName + ",jdbcType=" + jdbcType.name() + "}").append(",");
    }

    private static void buildUpdate(StringBuilder baseUpdateClause, DbColumn column, String propertyName, JdbcType jdbcType, boolean appendSplit) {
        baseUpdateClause.append(column.getName() + " = #{" + propertyName + ",jdbcType=" + jdbcType.name() + "}");
        if (appendSplit) {
            baseUpdateClause.append(",\n            ");
        }
    }

    private static void buildUpdateSelective(StringBuilder baseUpdateClause, DbColumn column,
                                             String propertyName, JdbcType jdbcType) {
        baseUpdateClause.append("<if test=\"" + propertyName + " != null\">" + column.getName() + " = #{" + propertyName + ",jdbcType=" + jdbcType.name() + "},</if>\n          ");
    }

    private static String typeMapping(String dataType, Set<String> importSet) {
        String javaType;
        if (JdbcType.BIGINT.name().equalsIgnoreCase(dataType)) {
            javaType = "Long";
        } else if (JdbcType.VARCHAR.name().equalsIgnoreCase(dataType)) {
            javaType = "String";
        } else if (JdbcType.INTEGER.name().equalsIgnoreCase(dataType)) {
            javaType = "Integer";
        } else if (JdbcType.TINYINT.name().equalsIgnoreCase(dataType)) {
            javaType = "Integer";
        } else if (JdbcType.SMALLINT.name().equalsIgnoreCase(dataType)) {
            javaType = "Integer";
        } else if ("int".equalsIgnoreCase(dataType)) {
            javaType = "Integer";
        } else if ("tinyint".equalsIgnoreCase(dataType)) {
            javaType = "Integer";
        } else if ("smallint".equalsIgnoreCase(dataType)) {
            javaType = "Integer";
        } else if ("datetime".equalsIgnoreCase(dataType)) {
            javaType = "Date";
            importSet.add("import java.util.Date;");
        } else if (JdbcType.TIMESTAMP.name().equalsIgnoreCase(dataType)) {
            javaType = "Date";
            importSet.add("import java.util.Date;");
        } else if (JdbcType.DATE.name().equalsIgnoreCase(dataType)) {
            javaType = "Date";
            importSet.add("import java.util.Date;");
        } else if (JdbcType.DECIMAL.name().equalsIgnoreCase(dataType)) {
            javaType = "BigDecimal";
            importSet.add("import java.math.BigDecimal;");
        } else if (JdbcType.DOUBLE.name().equalsIgnoreCase(dataType)) {
            javaType = "Double";
        } else {
            javaType = dataType;
        }
        return javaType;
    }

    /**
     * 数据库类型转换为mybatis类型
     *
     * @param dataType
     * @return
     */
    private static JdbcType getJdbcType(String dataType) {
        JdbcType jdbcType = null;
        if (JdbcType.BIGINT.name().equalsIgnoreCase(dataType)) {
            jdbcType = JdbcType.BIGINT;
        } else if (JdbcType.VARCHAR.name().equalsIgnoreCase(dataType)) {
            jdbcType = JdbcType.VARCHAR;
        } else if (JdbcType.INTEGER.name().equalsIgnoreCase(dataType)) {
            jdbcType = JdbcType.INTEGER;
        } else if (JdbcType.TINYINT.name().equalsIgnoreCase(dataType)) {
            jdbcType = JdbcType.TINYINT;
        } else if (JdbcType.SMALLINT.name().equalsIgnoreCase(dataType)) {
            jdbcType = JdbcType.SMALLINT;
        } else if ("int".equalsIgnoreCase(dataType)) {
            jdbcType = JdbcType.INTEGER;
        } else if ("datetime".equalsIgnoreCase(dataType)) {
            jdbcType = JdbcType.TIMESTAMP;
        } else if (JdbcType.TIMESTAMP.name().equalsIgnoreCase(dataType)) {
            jdbcType = JdbcType.TIMESTAMP;
        } else if (JdbcType.DATE.name().equalsIgnoreCase(dataType)) {
            jdbcType = JdbcType.DATE;
        } else if (JdbcType.DECIMAL.name().equalsIgnoreCase(dataType)) {
            jdbcType = JdbcType.DECIMAL;
        } else if (JdbcType.DOUBLE.name().equalsIgnoreCase(dataType)) {
            jdbcType = JdbcType.DOUBLE;
        }
        return jdbcType;
    }

    private static String getModelName(String tablePrefix, DbTable dbTable) {
        String tableName = dbTable.getName();
        if (StringUtils.isNotBlank(tablePrefix)) {
            tableName = tableName.replace(tablePrefix, "");
        }
        return WordUtils.capitalizeFully(tableName, new char[]{'_'}).replace("_", "");
    }

    private static String formatName(String dbName) {
        String name = StringUtils.replace(dbName, "_", " ");
        name = StringUtils.lowerCase(name);
        name = WordUtils.capitalize(name);
        name = StringUtils.replace(name, " ", "");
        return name;
    }


    /**
     * 获取文件名
     */
    private static String getFileName(String template, String className, String packageName, String moduleName) {
        String javaPath = "main" + File.separator + "java" + File.separator;
        String xmlPath = "main" + File.separator + "resources" + File.separator + "mapper" + File.separator;
        String frontPath = "webapp" + File.separator;
        if (StringUtils.isNotBlank(packageName)) {
            javaPath += packageName.replace(".", File.separator) + File.separator;
        }
        if (template.contains("index.html.vm")) {
            return frontPath + "views" + File.separator + moduleName + File.separator + "index.html";
        }
        if (template.contains("list.html.vm")) {
            return frontPath + "views" + File.separator + moduleName + File.separator + "list.html";
        }

        if (template.contains("Mapper.xml.vm")) {
            return xmlPath + className + "Mapper.xml";
        }
        if (template.contains("Model.java.vm")) {
            return javaPath + "model" + File.separator + className + ".java";
        }
        if (template.contains("Dto.java.vm")) {
            return javaPath + "dto" + File.separator + className + "Dto.java";
        }

        if (template.contains("Service.java.vm")) {
            return javaPath + "service" + File.separator + className + "Service.java";
        }
        if (template.contains("ServiceImpl.java.vm")) {
            return javaPath + "service" + File.separator + "impl" + File.separator + className + "ServiceImpl.java";
        }

        if (template.contains("Controller.java.vm")) {
            return javaPath + "controller" + File.separator + className + "Controller.java";
        }
        return null;
    }

    private static void build(String templateName, String modelName, String packageName, String requestName,
                              VelocityContext dataModel, ZipOutputStream zip) {
        //渲染模板
        StringWriter sw = new StringWriter();
        Template template = Velocity.getTemplate(templateName, "UTF-8");
        template.merge(dataModel, sw);
        try {
            //添加到zip
            zip.putNextEntry(new ZipEntry(getFileName(templateName, modelName, packageName, requestName)));
            IOUtils.write(sw.toString(), zip, "UTF-8");
            IOUtils.closeQuietly(sw);
            zip.closeEntry();
        } catch (IOException e) {
            throw new ZeusGenException("渲染模板失败，表名：" + modelName, e);
        }
    }

}
